/*
 * 02/19/2006
 *
 * SearchEngine.java - Handles find/replace operations in an RTextArea.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package org.fife.ui.rtextarea;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.JTextArea;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;

import org.fife.ui.rsyntaxtextarea.DocumentRange;
import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities;


/**
 * A singleton class that can perform advanced find/replace operations
 * in an {@link RTextArea}.  Simply create a {@link SearchContext} and call
 * one of the following methods:
 * 
 * <ul>
 *    <li>{@link #find(JTextArea, SearchContext)}
 *    <li>{@link #replace(RTextArea, SearchContext)}
 *    <li>{@link #replaceAll(RTextArea, SearchContext)}
 *    <li>{@link #markAll(RTextArea, SearchContext)}
 * </ul>
 *
 * @author Robert Futrell
 * @version 1.0
 * @see SearchContext
 */
public class SearchEngine {


    /**
     * Private constructor to prevent instantiation.
     */
    private SearchEngine() {
    }


    /**
     * Finds the next instance of the string/regular expression specified
     * from the caret position.  If a match is found, it is selected in this
     * text area.
     *
     * @param textArea The text area in which to search.
     * @param context What to search for and all search options.
     * @return The result of the operation.
     * @throws PatternSyntaxException If this is a regular expression search
     *         but the search text is an invalid regular expression.
     * @see #replace(RTextArea, SearchContext)
     * @see #replaceAll(RTextArea, SearchContext)
     */
    public static SearchResult find(JTextArea textArea, SearchContext context) {

        // Always clear previous "mark all" highlights
        if (textArea instanceof RTextArea || context.getMarkAll()) {
            ((RTextArea)textArea).clearMarkAllHighlights();
        }
        boolean doMarkAll = textArea instanceof RTextArea && context.getMarkAll();

        String text = context.getSearchFor();
        if (text==null || text.length()==0) {
            if (doMarkAll) {
                // Force "mark all" event to be broadcast so listeners know to
                // clear their mark-all markers.  The RSTA already cleared its
                // highlights above, but cleraMarkAllHighlights() doesn't firs
                // an event itself for performance reasons.
                List<DocumentRange> emptyRangeList = Collections.emptyList();
                ((RTextArea)textArea).markAll(emptyRangeList);
            }
            return new SearchResult();
        }

        // Be smart about what position we're "starting" at.  We don't want
        // to find a match in the currently selected text (if any), so we
        // start searching AFTER the selection if searching forward, and
        // BEFORE the selection if searching backward.
        Caret c = textArea.getCaret();
        boolean forward = context.getSearchForward();
        int start = forward ? Math.max(c.getDot(), c.getMark()) :
                        Math.min(c.getDot(), c.getMark());

        String findIn = getFindInText(textArea, start, forward);
        if (findIn==null || findIn.length()==0) {
            return new SearchResult();
        }

        int markAllCount = 0;
        if (doMarkAll) {
            markAllCount = markAllImpl((RTextArea)textArea, context).
                    getMarkedCount();
        }

        SearchResult result = SearchEngine.findImpl(findIn, context);
        if (result.wasFound() && !result.getMatchRange().isZeroLength()) {
            // Without this, if JTextArea isn't in focus, selection
            // won't appear selected.
            textArea.getCaret().setSelectionVisible(true);
            if (forward && start>-1) {
                result.getMatchRange().translate(start);
            }
            RSyntaxUtilities.selectAndPossiblyCenter(textArea,
                    result.getMatchRange(), true);
        }

        result.setMarkedCount(markAllCount);
        return result;

    }


    /**
     * Finds the next instance of the string/regular expression specified
     * from the caret position.  If a match is found, it is selected in this
     * text area.
     *
     * @param findIn The text to search in.
     * @param context The search context.
     * @return The result of the operation.  "Mark all" will always be zero,
     *         since this method does not perform that operation.
     * @throws PatternSyntaxException If this is a regular expression search
     *         but the search text is an invalid regular expression.
     */
    private static SearchResult findImpl(String findIn, SearchContext context) {

        String text = context.getSearchFor();
        boolean forward = context.getSearchForward();

        // Find the next location of the text we're searching for.
        DocumentRange range = null;
        if (!context.isRegularExpression()) {
            int pos = getNextMatchPos(text, findIn, forward,
                                context.getMatchCase(), context.getWholeWord());
            findIn = null; // May help garbage collecting.
            if (pos!=-1) {
                range = new DocumentRange(pos, pos+text.length());
            }
        }

        else {
            // Regex matches can have varying widths.  The returned point's
            // x- and y-values represent the start and end indices of the
            // match in findIn.
            Point regExPos = null;
            int start = 0;
            do {
                regExPos = getNextMatchPosRegEx(text, findIn.substring(start),
                    forward, context.getMatchCase(), context.getWholeWord());
                if (regExPos!=null) {
                    if (regExPos.x!=regExPos.y) {
                        regExPos.translate(start, start);
                        range = new DocumentRange(regExPos.x, regExPos.y);
                    }
                    else {
                        start += regExPos.x + 1;
                    }
                }
            } while (start<findIn.length() && regExPos!=null && range==null);//while (emptyMatchFound);
        }

        if (range!=null) {
            return new SearchResult(range, 1, 0);
        }
        return new SearchResult();

    }


    /**
     * Returns a <code>CharSequence</code> for a text area that doesn't make a
     * copy of its contents for iteration.  This conserves memory but is likely
     * just a tad slower.
     *
     * @param textArea The text area whose document is the basis for the
     *        <code>CharSequence</code>.
     * @param start The starting offset of the sequence (or ending offset if
     *        <code>forward</code> is <code>false</code>).
     * @param forward Whether we're searching forward or backward.
     * @return The character sequence.
     */
    private static CharSequence getFindInCharSequence(RTextArea textArea,
            int start, boolean forward) {
        RDocument doc = (RDocument)textArea.getDocument();
        int csStart = 0;
        int csEnd = 0;
        if (forward) {
            csStart = start;
            csEnd = doc.getLength();
        }
        else {
            csStart = 0;
            csEnd = start;
        }
        return new RDocumentCharSequence(doc, csStart, csEnd);
    }


    /**
     * Returns the text in which to search, as a string.  This is used
     * internally to grab the smallest buffer possible in which to search.
     */
    private static String getFindInText(JTextArea textArea, int start,
                                    boolean forward) {

        // Be smart about the text we grab to search in.  We grab more than
        // a single line because our searches can return multi-line results.
        // We copy only the chars that will be searched through.
        String findIn = null;
        try {
            if (forward) {
                findIn = textArea.getText(start,
                            textArea.getDocument().getLength()-start);
            }
            else { // backward
                findIn = textArea.getText(0, start);
            }
        } catch (BadLocationException ble) {
            // Never happens; findIn will be null anyway.
            ble.printStackTrace();
        }

        return findIn;

    }


    /**
     * This method is called internally by
     * <code>getNextMatchPosRegExImpl</code> and is used to get the locations
     * of all regular-expression matches, and possibly their replacement
     * strings.<p>
     *
     * Returns either:
     * <ul>
     *   <li>A list of points representing the starting and ending positions
     *       of all matches returned by the specified matcher, or
     *   <li>A list of <code>RegExReplaceInfo</code>s describing the matches
     *       found by the matcher and the replacement strings for each.
     * </ul>
     *
     * If <code>replacement</code> is <code>null</code>, this method call is
     * assumed to be part of a "find" operation and points are returned.  If
     * if is non-<code>null</code>, it is assumed to be part of a "replace"
     * operation and the <code>RegExReplaceInfo</code>s are returned.<p>
     *
     * @param m The matcher.
     * @param replaceStr The string to replace matches with.  This is a
     *        "template" string and can contain captured group references in
     *        the form "<code>${digit}</code>".
     * @return A list of result objects.
     * @throws IndexOutOfBoundsException If <code>replaceStr</code> references
     *         an invalid group (less than zero or greater than the number of
     *         groups matched).
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    private static List getMatches(Matcher m, String replaceStr) {
        ArrayList matches = new ArrayList();
        while (m.find()) {
            Point loc = new Point(m.start(), m.end());
            if (replaceStr==null) { // Find, not replace.
                matches.add(loc);
            }
            else { // Replace.
                matches.add(new RegExReplaceInfo(m.group(0), loc.x, loc.y,
                                getReplacementText(m, replaceStr)));
            }
        }
        return matches;
    }


    /**
     * Searches <code>searchIn</code> for an occurrence of
     * <code>searchFor</code> either forwards or backwards, matching
     * case or not.<p>
     *
     * Most clients will have no need to call this method directly.
     *
     * @param searchFor The string to look for.
     * @param searchIn The string to search in.
     * @param forward Whether to search forward or backward in
     *        <code>searchIn</code>.
     * @param matchCase If <code>true</code>, do a case-sensitive search for
     *        <code>searchFor</code>.
     * @param wholeWord If <code>true</code>, <code>searchFor</code>
     *        occurrences embedded in longer words in <code>searchIn</code>
     *        don't count as matches.
     * @return The starting position of a match, or <code>-1</code> if no
     *         match was found.
     */
    public static final int getNextMatchPos(String searchFor, String searchIn,
                                boolean forward, boolean matchCase,
                                boolean wholeWord) {

        // Make our variables lower case if we're ignoring case.
        if (!matchCase) {
            return getNextMatchPosImpl(searchFor.toLowerCase(),
                                searchIn.toLowerCase(), forward,
                                matchCase, wholeWord);
        }

        return getNextMatchPosImpl(searchFor, searchIn, forward,
                                matchCase, wholeWord);

    }


    /**
     * Actually does the work of matching; assumes searchFor and searchIn
     * are already upper/lower-cased appropriately.<br>
     * The reason this method is here is to attempt to speed up
     * <code>FindInFilesDialog</code>; since it repeatedly calls
     * this method instead of <code>getNextMatchPos</code>, it gets better
     * performance as it no longer has to allocate a lower-cased string for
     * every call.
     *
     * @param searchFor The string to search for.
     * @param searchIn The string to search in.
     * @param goForward Whether the search is forward or backward.
     * @param matchCase Whether the search is case-sensitive.
     * @param wholeWord Whether only whole words should be matched.
     * @return The location of the next match, or <code>-1</code> if no
     *         match was found.
     */
    private static final int getNextMatchPosImpl(String searchFor,
                                String searchIn, boolean goForward,
                                boolean matchCase, boolean wholeWord) {

        if (wholeWord) {
            int len = searchFor.length();
            int temp = goForward ? 0 : searchIn.length();
            int tempChange = goForward ? 1 : -1;
            while (true) {
                if (goForward)
                    temp = searchIn.indexOf(searchFor, temp);
                else
                    temp = searchIn.lastIndexOf(searchFor, temp);
                if (temp!=-1) {
                    if (isWholeWord(searchIn, temp, len)) {
                        return temp;
                    }
                    else {
                        temp += tempChange;
                        continue;
                    }
                }
                return temp; // Always -1.
            }
        }
        else {
            return goForward ? searchIn.indexOf(searchFor) :
                            searchIn.lastIndexOf(searchFor);
        }

    }


    /**
     * Searches <code>searchIn</code> for an occurrence of <code>regEx</code>
     * either forwards or backwards, matching case or not.
     *
     * @param regEx The regular expression to look for.
     * @param searchIn The string to search in.
     * @param goForward Whether to search forward.  If <code>false</code>,
     *        search backward.
     * @param matchCase Whether or not to do a case-sensitive search for
     *        <code>regEx</code>.
     * @param wholeWord If <code>true</code>, <code>regEx</code>
     *        occurrences embedded in longer words in <code>searchIn</code>
     *        don't count as matches.
     * @return A <code>Point</code> representing the starting and ending
     *         position of the match, or <code>null</code> if no match was
     *         found.
     * @throws PatternSyntaxException If <code>regEx</code> is an invalid
     *         regular expression.
     * @see #getNextMatchPos
     */
    private static Point getNextMatchPosRegEx(String regEx,
                            CharSequence searchIn, boolean goForward,
                            boolean matchCase, boolean wholeWord) {
        return (Point)getNextMatchPosRegExImpl(regEx, searchIn, goForward,
                                    matchCase, wholeWord, null);
    }


    /**
     * Searches <code>searchIn</code> for an occurrence of <code>regEx</code>
     * either forwards or backwards, matching case or not.
     *
     * @param regEx The regular expression to look for.
     * @param searchIn The string to search in.
     * @param goForward Whether to search forward.  If <code>false</code>,
     *        search backward.
     * @param matchCase Whether or not to do a case-sensitive search for
     *        <code>regEx</code>.
     * @param wholeWord If <code>true</code>, <code>regEx</code>
     *        occurrences embedded in longer words in <code>searchIn</code>
     *        don't count as matches.
     * @param replaceStr The string that will replace the match found (if
     *        a match is found).  The object returned will contain the
     *        replacement string with matched groups substituted.  If this
     *        value is <code>null</code>, it is assumed this call is part of a
     *        "find" instead of a "replace" operation.
     * @return If <code>replaceStr</code> is <code>null</code>, a
     *         <code>Point</code> representing the starting and ending points
     *         of the match.  If it is non-<code>null</code>, an object with
     *         information about the match and the morphed string to replace
     *         it with.  If no match is found, <code>null</code> is returned.
     * @throws PatternSyntaxException If <code>regEx</code> is an invalid
     *         regular expression.
     * @throws IndexOutOfBoundsException If <code>replaceStr</code> references
     *         an invalid group (less than zero or greater than the number of
     *         groups matched).
     * @see #getNextMatchPos
     */
    private static Object getNextMatchPosRegExImpl(String regEx,
                            CharSequence searchIn, boolean goForward,
                            boolean matchCase, boolean wholeWord,
                            String replaceStr) {

        if (wholeWord) {
            regEx = "\\b" + regEx + "\\b";
        }

        // Make a pattern that takes into account whether or not to match case.
        int flags = Pattern.MULTILINE; // '^' and '$' are done per line.
        flags = RSyntaxUtilities.getPatternFlags(matchCase, flags);
        Pattern pattern = null;
        try {
            pattern = Pattern.compile(regEx, flags);
        } catch (PatternSyntaxException pse) {
            return null; // e.g. a "mark all" request with incomplete regex
        }

        // Make a Matcher to find the regEx instances.
        Matcher m = pattern.matcher(searchIn);

        // Search forwards
        if (goForward) {
            while (m.find()) {
                if (replaceStr == null) { // Find, not replace.
                    return new Point(m.start(), m.end());
                } else if (m.end() != 0) {
                    // Guard against infinite matches by only returning info if the matcher end is nonzero.
                    return new RegExReplaceInfo(m.group(0), m.start(), m.end(), getReplacementText(m, replaceStr));
                }
            }
        }

        // Search backwards
        else {
            List<?> matches = getMatches(m, replaceStr);
            if (!matches.isEmpty()) {
                return matches.get(matches.size()-1);
            }
        }

        return null; // No match found

    }


    /**
     * Returns information on how to implement a regular expression "replace"
     * action in the specified text with the specified replacement string.
     *
     * @param searchIn The string to search in.
     * @param context The search options.
     * @return A <code>RegExReplaceInfo</code> object describing how to
     *         implement the replace.
     * @throws PatternSyntaxException If the search text is an invalid regular
     *         expression.
     * @throws IndexOutOfBoundsException If the replacement text references an
     *         invalid group (less than zero or greater than the number of
     *         groups matched).
     * @see #getNextMatchPos
     */
    private static RegExReplaceInfo getRegExReplaceInfo(CharSequence searchIn,
                                        SearchContext context) {
        // Can't pass null to getNextMatchPosRegExImpl or it'll think
        // you're doing a "find" operation instead of "replace, and return a
        // Point.
        String replacement = context.getReplaceWith();
        if (replacement==null) {
            replacement = "";
        }
        String regex = context.getSearchFor();
        boolean goForward = context.getSearchForward();
        boolean matchCase = context.getMatchCase();
        boolean wholeWord = context.getWholeWord();
        return (RegExReplaceInfo)getNextMatchPosRegExImpl(regex, searchIn,
                        goForward, matchCase, wholeWord, replacement);
    }


    /**
     * Called internally by <code>getMatches()</code>.  This method assumes
     * that the specified matcher has just found a match, and that you want
     * to get the string with which to replace that match.<p>
     *
     * Escapes simply insert the escaped character, except for <code>\n</code>
     * and <code>\t</code>, which insert a newline and tab respectively.
     * Substrings of the form <code>$\d+</code> are considered to be matched
     * groups.  To include a literal dollar sign in your template, escape it
     * (i.e. <code>\$</code>).<p>
     *
     * Most clients will have no need to call this method directly.
     *
     * @param m The matcher.
     * @param template The template for the replacement string.  For example,
     *        "<code>foo</code>" would yield the replacement string
     *        "<code>foo</code>", while "<code>$1 is the greatest</code>"
     *        would yield different values depending on the value of the first
     *        captured group in the match.
     * @return The string to replace the match with.
     * @throws IndexOutOfBoundsException If <code>template</code> references
     *         an invalid group (less than zero or greater than the number of
     *         groups matched).
     */
    public static String getReplacementText(Matcher m, CharSequence template) {

        // NOTE: This code was mostly ripped off from J2SE's Matcher
        // class.

        // Process substitution string to replace group references with groups
        int cursor = 0;
        StringBuilder result = new StringBuilder();

        while (cursor < template.length()) {

            char nextChar = template.charAt(cursor);

            if (nextChar == '\\') { // Escape character.
                nextChar = template.charAt(++cursor);
                switch (nextChar) { // Special cases.
                    case 'n':
                        nextChar = '\n';
                        break;
                    case 't':
                        nextChar = '\t';
                        break;
                }
                result.append(nextChar);
                cursor++;
            }
            else if (nextChar == '$') { // Group reference.

                cursor++; // Skip the '$'.

                // The first number is always a group
                int refNum = template.charAt(cursor) - '0';
                if ((refNum < 0)||(refNum > 9)) {
                    // This should really be an IllegalArgumentException,
                    // but we cheat to keep all "group" errors throwing
                    // the same exception type.
                    throw new IndexOutOfBoundsException(
                                "No group " + template.charAt(cursor));
                }
                cursor++;

                // Capture the largest legal group string
                boolean done = false;
                while (!done) {
                    if (cursor >= template.length()) {
                        break;
                    }
                    int nextDigit = template.charAt(cursor) - '0';
                    if ((nextDigit < 0)||(nextDigit > 9)) { // not a number
                        break;
                    }
                    int newRefNum = (refNum * 10) + nextDigit;
                    if (m.groupCount() < newRefNum) {
                        done = true;
                    }
                    else {
                        refNum = newRefNum;
                        cursor++;
                    }
                }

                // Append group
                if (m.group(refNum) != null)
                    result.append(m.group(refNum));

            }

            else {
                result.append(nextChar);
                cursor++;
            }

        }

        return result.toString();

    }


    /**
     * Returns whether the characters on either side of
     * <code>substr(searchIn, startPos, startPos+searchStringLength)</code>
     * are <em>not</em> letters or digits.
     */
    private static final boolean isWholeWord(CharSequence searchIn,
                                            int offset, int len) {

        boolean wsBefore, wsAfter;

        try {
            wsBefore = !Character.isLetterOrDigit(searchIn.charAt(offset - 1));
        } catch (IndexOutOfBoundsException e) { wsBefore = true; }
        try {
            wsAfter  = !Character.isLetterOrDigit(searchIn.charAt(offset + len));
        } catch (IndexOutOfBoundsException e) { wsAfter = true; }

        return wsBefore && wsAfter;

    }


    /**
     * Makes the caret's dot and mark the same location so that, for the
     * next search in the specified direction, a match will be found even
     * if it was within the original dot and mark's selection.
     *
     * @param textArea The text area.
     * @param forward Whether the search will be forward through the
     *        document (<code>false</code> means backward).
     * @return The new dot and mark position.
     */
    private static final int makeMarkAndDotEqual(JTextArea textArea,
                                        boolean forward) {
        Caret c = textArea.getCaret();
        int val = forward ? Math.min(c.getDot(), c.getMark()) :
                        Math.max(c.getDot(), c.getMark());
        c.setDot(val);
        return val;
    }


    /**
     * Marks all instances of the specified text in this text area.  This
     * method is typically only called directly in response to search events
     * of type <code>SearchEvent.Type.MARK_ALL</code>.  "Mark all" behavior
     * is automatically performed when {@link #find(JTextArea, SearchContext)}
     * or {@link #replace(RTextArea, SearchContext)} is called.
     *
     * @param textArea The text area in which to mark occurrences.
     * @param context The search context specifying the text to search for.
     *        It is assumed that <code>context.getMarkAll()</code> has already
     *        been checked and returns <code>true</code>.
     * @return The results of the operation.
     */
    public static final SearchResult markAll(RTextArea textArea,
            SearchContext context) {
        textArea.clearMarkAllHighlights();
//      if (context.getMarkAll()) {
            return markAllImpl(textArea, context);
//      }
//      return new SearchResult();
    }


    /**
     * Marks all instances of the specified text in this text area.  This
     * method is typically only called directly in response to search events
     * of type <code>SearchEvent.Type.MARK_ALL</code>.  "Mark all" behavior
     * is automatically performed when {@link #find(JTextArea, SearchContext)}
     * or #replace(RTextArea, SearchContext) is called.
     *
     * @param textArea The text area in which to mark occurrences.
     * @param context The search context specifying the text to search for.
     *        It is assumed that <code>context.getMarkAll()</code> has already
     *        been checked and returns <code>true</code>.
     * @return The results of the operation.
     */
    private static final SearchResult markAllImpl(RTextArea textArea,
            SearchContext context) {

        String toMark = context.getSearchFor();
        int markAllCount = 0;

        // context.getMarkAll()==false => clear "mark all" highlights
        if (context.getMarkAll() && toMark!=null && toMark.length()>0
                /*&& !toMark.equals(markedWord)*/) {

            List<DocumentRange> highlights = new ArrayList<DocumentRange>();
            context = context.clone();
            context.setSearchForward(true);
            context.setMarkAll(false);

            String findIn = textArea.getText();
            int start = 0;

            // Optimization for pedantic worst-case of thousands of matches
            // while doing a case-insensitive search.  Go ahead and normalize
            // case just once, so that toLowerCase() isn't done for toMark and
            // findIn for each match found.
            if (!context.getMatchCase()) {
                context.setMatchCase(true);
                context.setSearchFor(toMark.toLowerCase());
                findIn = findIn.toLowerCase();
            }

            SearchResult res = SearchEngine.findImpl(findIn, context);
            while (res.wasFound()) {
                DocumentRange match = res.getMatchRange().translate(start);
                if (match.isZeroLength()) {
                    // Searched for a regex like "foo|".  The "empty string"
                    // part of the regex matches space between chars.  We want
                    // to skip these in the case of mark-all.
                    start = match.getEndOffset() + 1;
                    if (start>findIn.length()) {
                        break;
                    }
                }
                else {
                    highlights.add(match);
                    start = match.getEndOffset();
                }
                res = SearchEngine.findImpl(findIn.substring(start), context);
            }
            textArea.markAll(highlights);
            markAllCount = highlights.size();
        }

        else {
            // Force a repaint of "mark all" highlights so ErrorStrips can
            // get updated
            List<DocumentRange> empty = Collections.emptyList();
            textArea.markAll(empty);
        }

        return new SearchResult(null, 0, markAllCount);

    }


    /**
     * Finds the next instance of the regular expression specified from
     * the caret position.  If a match is found, it is replaced with
     * the specified replacement string.
     *
     * @param textArea The text area in which to search.
     * @param context What to search for and all search options.
     * @return The result of the operation.
     * @throws PatternSyntaxException If this is a regular expression search
     *         but the search text is an invalid regular expression.
     * @throws IndexOutOfBoundsException If this is a regular expression search
     *         but the replacement text references an invalid group (less than
     *         zero or greater than the number of groups matched).
     * @see #replace(RTextArea, SearchContext)
     * @see #find(JTextArea, SearchContext)
     */
    private static SearchResult regexReplace(RTextArea textArea,
            SearchContext context) throws PatternSyntaxException {

        // Be smart about what position we're "starting" at.  For example,
        // if they are searching backwards and there is a selection such that
        // the dot is past the mark, and the selection is the text for which
        // you're searching, this search will find and return the current
        // selection.  So, in that case we start at the beginning of the
        // selection.
        Caret c = textArea.getCaret();
        boolean forward = context.getSearchForward();
        int start = makeMarkAndDotEqual(textArea, forward);

        CharSequence findIn = getFindInCharSequence(textArea, start, forward);
        if (findIn==null) return new SearchResult();

        int markAllCount = 0;
        if (context.getMarkAll()) {
            markAllCount = markAllImpl(textArea, context).getMarkedCount();
        }

        // Find the next location of the text we're searching for.
        RegExReplaceInfo info = getRegExReplaceInfo(findIn, context);

        // If a match was found, do the replace and return!
        DocumentRange range = null;
        if (info!=null) {

            // Without this, if JTextArea isn't in focus, selection won't
            // appear selected.
            c.setSelectionVisible(true);

            int matchStart = info.getStartIndex();
            int matchEnd = info.getEndIndex();
            if (forward) {
                matchStart += start;
                matchEnd += start;
            }
            textArea.setSelectionStart(matchStart);
            textArea.setSelectionEnd(matchEnd);
            String replacement = info.getReplacement();
            textArea.replaceSelection(replacement);

            // If there is another match, find and select it.
            int dot = matchStart + replacement.length();
            findIn = getFindInCharSequence(textArea, dot, forward);
            info = getRegExReplaceInfo(findIn, context);
            if (info!=null) {
                matchStart = info.getStartIndex();
                matchEnd = info.getEndIndex();
                if (forward) {
                    matchStart += dot;
                    matchEnd += dot;
                }
                range = new DocumentRange(matchStart, matchEnd);
            }
            else {
                range = new DocumentRange(dot, dot);
            }
            RSyntaxUtilities.selectAndPossiblyCenter(textArea, range, true);

        }

        int count = range!=null ? 1 : 0;
        return new SearchResult(range, count, markAllCount);

    }


    /**
     * Finds the next instance of the text/regular expression specified from
     * the caret position.  If a match is found, it is replaced with the
     * specified replacement string.
     *
     * @param textArea The text area in which to search.
     * @param context What to search for and all search options.
     * @return The result of the operation.
     * @throws PatternSyntaxException If this is a regular expression search
     *         but the search text is an invalid regular expression.
     * @throws IndexOutOfBoundsException If this is a regular expression search
     *         but the replacement text references an invalid group (less than
     *         zero or greater than the number of groups matched).
     * @see #replaceAll(RTextArea, SearchContext)
     * @see #find(JTextArea, SearchContext)
     */
    public static SearchResult replace(RTextArea textArea,
            SearchContext context) throws PatternSyntaxException {

        // Always clear previous "mark all" highlights
        if (context.getMarkAll()) {
            textArea.clearMarkAllHighlights();
        }

        String toFind = context.getSearchFor();
        if (toFind==null || toFind.length()==0) {
            return new SearchResult();
        }

        textArea.beginAtomicEdit();
        try {

            // Regular expression replacements have their own method.
            if (context.isRegularExpression()) {
                return regexReplace(textArea, context);
            }

            // Plain text search.  If we find it, replace it!
            // First make the dot and mark equal (get rid of any selection), as
            // a common use-case is the user will use "Find" to select the text
            // to replace, then click "Replace" to replace the current
            // selection. Since our find() method searches from an endpoint of
            // the selection, we must remove the selection to work properly.
            makeMarkAndDotEqual(textArea, context.getSearchForward());
            SearchResult res = find(textArea, context);

            if (res.wasFound() && !res.getMatchRange().isZeroLength()) {

                // Do the replacement
                String replacement = context.getReplaceWith();
                textArea.replaceSelection(replacement);

                // Move the caret to the right position.
                int dot = res.getMatchRange().getStartOffset();
                if (context.getSearchForward()) {
                    int length = replacement==null ? 0 : replacement.length();
                    dot += length;
                }
                textArea.setCaretPosition(dot);

                SearchResult next = find(textArea, context);
                DocumentRange range;
                if (next.wasFound()) {
                    range = next.getMatchRange();
                }
                else {
                    range = new DocumentRange(dot, dot);
                }
                res.setMatchRange(range);
                RSyntaxUtilities.selectAndPossiblyCenter(textArea, range, true);

            }

            return res;

        } finally {
            textArea.endAtomicEdit();
        }

    }


    /**
     * Replaces all instances of the text/regular expression specified in
     * the specified document with the specified replacement.
     *
     * @param textArea The text area in which to search.
     * @param context What to search for and all search options.
     * @return The result of the operation.
     * @throws PatternSyntaxException If this is a regular expression search
     *         but the replacement text is an invalid regular expression.
     * @throws IndexOutOfBoundsException If this is a regular expression search
     *         but the replacement text references an invalid group (less than
     *         zero or greater than the number of groups matched).
     * @see #replace(RTextArea, SearchContext)
     * @see #find(JTextArea, SearchContext)
     */
    public static SearchResult replaceAll(RTextArea textArea,
            SearchContext context) throws PatternSyntaxException {

        // Always clear previous "mark all" highlights
        if (context.getMarkAll()) {
            textArea.clearMarkAllHighlights();
        }

        context.setSearchForward(true); // Replace all always searches forward
        String toFind = context.getSearchFor();
        if (toFind==null || toFind.length()==0) {
            return new SearchResult();
        }

        // We call into SearchEngine.replace() multiple times, but we don't
        // want to perform a "mark all" ever.
        if (context.getMarkAll()) {
            context = context.clone();
            context.setMarkAll(false);
        }

        SearchResult lastFound = null;
        int count = 0;
        textArea.beginAtomicEdit();
        try {
            int oldOffs = textArea.getCaretPosition();
            textArea.setCaretPosition(0);
            SearchResult res = SearchEngine.replace(textArea, context);
            while (res.wasFound()) {
                lastFound = res;
                count++;
                res = SearchEngine.replace(textArea, context);
            }
            if (lastFound==null) { // If nothing was found, don't move the caret
                textArea.setCaretPosition(oldOffs);
                lastFound = new SearchResult();
            }
        } finally {
            textArea.endAtomicEdit();
        }

        lastFound.setCount(count);
        return lastFound;

    }


}